package com.example.java_gobang.game;

import com.example.java_gobang.model.UserInfo;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;

import java.io.IOException;
import java.util.LinkedList;
import java.util.Queue;

@Component
//匹配器,实现玩家匹配功能
public class Matcher {
    //通过队列来实现玩家之间的匹配模式。
    //按照不同的天梯积分分化成三个区域: normal   heigh    veryHeigh
    //所以只需要创建三个队列(粗糙)
    private Queue<UserInfo> normal = new LinkedList<>();
    private Queue<UserInfo> heigh = new LinkedList<>();
    private Queue<UserInfo> veryHeigh = new LinkedList<>();

    //在线玩家管理器
    @Autowired
    private OnlineUserManager onlineUserManager;

    //游戏房间管理器
    @Autowired
    private RoomManager roomManager;

    //JSON字符串转换对象
    private ObjectMapper mapper = new ObjectMapper();

    //进入匹配
    //因为多线程的操作会引起并发执行，此时多个队列可能会在两个线程或者多个线程中出现一个从队列中加入元素(玩家进入匹配)
    //另外的线程则对玩家进行退出匹配操作，此时就会引发并发问题，此时将每个不同水平的优先级队列的对象作为锁对象，来进行对同一个队列进行加锁操作
    //拿着不同的锁对象的线程不会互相干扰，此时也防止了多线程的并发执行,也就解决了线程安全问题
    public void add(UserInfo userInfo) {
        if (userInfo.getScore() < 2000) {
            synchronized (normal) {
                normal.offer(userInfo);
                normal.notify();
                System.out.println("玩家: " + userInfo.getUserName() + "加入匹配");
            }
        }else if (userInfo.getId() >= 2000 && userInfo.getScore() < 3000) {
            synchronized (heigh) {
                heigh.offer(userInfo);
                heigh.notify();
                System.out.println("玩家: " + userInfo.getUserName() + "加入匹配");
            }
        }else {
            synchronized (veryHeigh) {
                veryHeigh.offer(userInfo);
                veryHeigh.notify();
                System.out.println("玩家: " + userInfo.getUserName() + "加入匹配");
            }
        }
    }

    //退出匹配
    public void exit(UserInfo userInfo) {
        if (userInfo.getScore() < 2000) {
            synchronized (normal) {
                normal.remove(userInfo);
                System.out.println("玩家: " + userInfo.getUserName() + "退出匹配");
            }
        }else if (userInfo.getId() >= 2000 && userInfo.getScore() < 3000) {
            synchronized (heigh) {
                heigh.remove(userInfo);
                System.out.println("玩家: " + userInfo.getUserName() + "退出匹配");
            }
        }else {
            synchronized (veryHeigh) {
                veryHeigh.remove(userInfo);
                System.out.println("玩家: " + userInfo.getUserName() + "退出匹配");
            }
        }
    }

    //创建三个线程不断扫描三个不同的匹配队列，只要当队列中的元素凑成了一对玩家，就放到一个房间当中
    public Matcher() {
        //扫描正常分数的天梯积分玩家
        Thread t1 = new Thread(() -> {
            while (true) {
                handleMatch(normal);
            }
        });
        t1.start();

        //扫描高分数的天梯积分玩家
        Thread t2 = new Thread(() -> {
            while (true) {
                handleMatch(heigh);
            }
        });
        t2.start();


        //扫描很高分数的天梯积分玩家
        Thread t3 = new Thread(() -> {
            while (true) {
                handleMatch(veryHeigh);
            }
        });
        t3.start();

    }


    //扫描方法的主要逻辑
    private void handleMatch(Queue<UserInfo> queue) {
        //对方法进行加锁，通过传入的参数对象作为锁对象就不需要在多线程中每个线程加上锁的繁琐操作了。
        //并且不同的线程，拿着不同的锁对象则不会发生阻塞等待的效果
        synchronized (queue) {
            try {
                //1.如果此时队列中的玩家比两个小则返回
                //如果一直没有玩家进行匹配的话，则队列中的元素个数一直小于2，此时则返回到调用的方法的线程。
                //线程将扫描的方法执行完后（因为此时的玩家个数小于2，就直接返回了），线程又继续执行死循环中的方法
                // 此时如果一直没有玩家进入匹配，就会一直不断地忙等，会消耗大量的CPU资源
                //发生忙等问题，可以使用sleep，但是使用sleep的话不够完美，应该使用wait notify来进行解决忙等问题
                //但是如果还是使用if的话，此时如果加一个玩家后，唤醒了这个wait，但是此时还是不满足玩家个数大于两个的情况下，也是没有意义的
                //所以此时就需要使用while不断判断此时的玩家个数是否有一对儿了
                while (queue.size() < 2) {
                    queue.wait();
                }

                //2.此时队列中已经有两个玩家了，从队列中取出天梯积分相近的玩家
                UserInfo player1 = queue.poll();
                UserInfo player2 = queue.poll();
                System.out.println("拿出两个玩家");
                //打印玩家的日志
                System.out.println("匹配出两个玩家，玩家1: " + player1.getUserName() + "，玩家2: " + player2.getUserName());
                //3.获取玩家的WebSocket会话信息（WebSocketSession），获取会话信息的目的，是告诉玩家你匹配到对手了
                WebSocketSession session1 = onlineUserManager.getFromGameHall(player1.getId());
                WebSocketSession session2 = onlineUserManager.getFromGameHall(player2.getId());


                //判断玩家是否在线，如果按照前面的逻辑应该是存在的，此时只不过是为了再加一次判断确保程序的正确率更高
                if (session1 == null) {
                    //此时玩家1不在线，将玩家2返回到匹配队列中
                    queue.offer(player2);
                    return;
                }

                if (session2 == null) {
                    //此时玩家2不在线，将玩家1返回到匹配队列当中;
                    queue.offer(player1);
                    return;
                }

                //如果此时两个会话信息相同,代表两个玩家是同一个用户，也就是说多开的情况，虽然也不太可能会发生，但是多次进行判断更加保险
                if (session1 == session2) {
                    //将其中一个玩家返回匹配队列就行(两个玩家的信息都是一样的)
                    queue.offer(player1);
                    return;
                }

                //4.拿到两个玩家的信息，进入游戏房间
                //此时不在这里将两个玩家加入到房间中，是为了防止玩家在进入房间时
                //因为一些原因并没有真的进入房间，后续真的进入了游戏房间，则再将room中的玩家成员变量传入
                Room room = new Room();
                roomManager.add(room, player1.getId(), player2.getId());


                //5.匹配成功后给客户端发送响应
                //通过WebSocket返回一个matchSuccess的message
                MatchResponse response1 = new MatchResponse();
                response1.setOk(true);
                response1.setMessage("matchSuccess");
                String json1 = mapper.writeValueAsString(response1);
                session1.sendMessage(new TextMessage(json1));

                MatchResponse response2 = new MatchResponse();
                response2.setOk(true);
                response2.setMessage("matchSuccess");
                String json2 = mapper.writeValueAsString(response2);
                session2.sendMessage(new TextMessage(json2));
            }catch (IOException e) {
                e.printStackTrace();
            } catch (InterruptedException e) {
                //throw new RuntimeException(e);
                e.printStackTrace();
            }
        }
    }

}
